<?php
/*
 * Copyright (c) 2013 Eugene Kuzmenko
 *
 * Permission  is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in  the Software without restriction, including without limitation the rights
 * to  use,  copy,  modify,  merge, publish, distribute, sublicense, and/or sell
 * copies  of  the  Software,  and  to  permit  persons  to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The  above  copyright  notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE  SOFTWARE  IS  PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED,  INCLUDING  BUT  NOT  LIMITED  TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS  FOR  A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS  OR  COPYRIGHT  HOLDERS  BE  LIABLE  FOR  ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 * For more information, see <http://www.aljey.com>.
 */

namespace Al\Template\Lib;

use Al\Core\Exception\FileNotFoundException,
    Al\Template\Exception\InvalidTemplateEngineException,
    Al\Template\Exception\UnknownTemplateEngineException,
    Al\Core\Lib\Service;

/**
 * The main entry point for all the template related operations.
 *
 * @author Eugene Kuzmenko <mail@aljey.com>
 */
class Template {

  /**
   * @var string The extension of the last compiled template.
   */
  public $lastExtension;

  /**
   * @var array A collection of instantiated template engines.
   */
  private $engines = array();

  /**
   * Finds out the physical location of a template.
   *
   * @param string $template A colon separated template name
   *                         (eg. "Acme\Blog:entry:body.html.smarty"). The first
   *                         part of a template name is the name of the plugin
   *                         containing the template (the plugins Resources/view
   *                         directory), the second part is the path withing the
   *                         plugins view directory and the third part is the
   *                         name of the template file.
   *                         Omitting the first part would mean that the
   *                         template is located withing the project
   *                         app/Resources/view directory instead.
   *                         Omitting the second part means that the template is
   *                         located in the root of the view directory.
   *                         Omitting the third part of the plugin name is not
   *                         allowed.
   * @return string The physical location of the template.
   * @throws FileNotFoundException
   */
  public function findTemplate($template) {
    if (!$template || (3 !== count($tplParts = explode(':', $template)))) {
      throw new FileNotFoundException($template);
    }
    if ((empty($tplParts[0]) && is_file($tpl =
        "../app/Resources/view/$tplParts[1]/$tplParts[2]")) || is_file($tpl =
        '../app/Resources/view/' . str_replace('\\', '', $tplParts[0]) .
        "/$tplParts[1]/$tplParts[2]") ||
        is_file($tpl = Service::get('config')->getPluginLocation($tplParts[0]) .
        "/Resources/view/$tplParts[1]/$tplParts[2]") || is_file($tpl =
        '../src/' . str_replace('\\', '/', $tplParts[0]) .
        "/Resources/view/$tplParts[1]/$tplParts[2]")) {
      return $tpl;
    }
    throw new FileNotFoundException($template);
  }

  /**
   * Finds out the web location (path) of the requested public files
   * (<plugin>/Resources/public) formats them with the provided format and
   * returns the result.
   *
   * @param string $format [optional] @see sprintf()
   * @param array $params [optional] An array containing a "file" key with an
   *                      array of all of the required public resource
   *                      references.
   * @return string A combined response for all the resource references.
   * @throws FileNotFoundException
   */
  public function getAssets($format = '%s', $params = array()) {
    if (!isset($params['file'])) {
      throw new FileNotFoundException;
    }
    if (!is_array($params['file'])) $params['file'] = array($params['file']);
    $response = '';
    $config = Service::get('config');
    foreach ($params['file'] as $file) {
      if (preg_match('/^al:\/\//', $file)) {
        $file = ltrim($file, 'al://');
        $file_parts = explode(':', $file);
        if ((3 !== count($file_parts)) ||
            !$config->getPluginLocation($file_parts[0])) {
          throw new FileNotFoundException($file);
        }
        $file = $config->getPluginWebRoot($file_parts[0]) .
          "/$file_parts[1]/$file_parts[2]";
      }
      if (file_exists($path = ltrim($file, '/'))) {
        $parsed = parse_url($file);
        $query = isset($parsed['query']) ? $parsed['query'] : '';
        $file = $parsed['path'] . '?' . (false === strpos($query, '__fmod') ? (
          $query ? "$query&" : '') . '__fmod=' . filemtime($path) : $query);
      }
      $response .= sprintf($format, $file);
    }
    return $response;
  }

  /**
   * Returns a template engine instance.
   *
   * @param string $name A name of a template engine and a subsequent template
   *                     file name extension.
   * @return AbstractTemplateEngine A template engine instance.
   * @throws UnknownTemplateEngineException
   * @throws InvalidTemplateEngineException
   */
  public function getEngine($name) {
    if (!isset($this->engines[$name])) {
      if (!($config = Service::get(
          "config.plugins.Al\\Template.driver.$name"))) {
        throw new UnknownTemplateEngineException($name);
      }
      $this->engines[$name] = new $config;
      if (!($this->engines[$name] instanceof AbstractTemplateEngine)) {
        throw new InvalidTemplateEngineException($name);
      }
    }
    return $this->engines[$name];
  }

  /**
   * Removes unnecessary whitespace from an html string.
   * Minifies inline JavaScript and CSS.
   * Makes sure compile time php code is unaffected.
   *
   * @param string $html
   * @return string
   */
  public function trimHTML($html = '') {
    $php = array();
    $placeholder = array();
    $i = 0;
    $minify = Service::get('minify');
    $js = $minify['js'];
    $css = $minify['css'];
    $html = preg_replace_callback('/\s*<\?.*?\?>\s*/s', function ($matches)
    use (&$php, &$placeholder, &$i) {
      $php[] = trim($matches[0]);
      $placeholder[] = $p = '__PHP_' . $i++ . '__';
      return $p;
    }, $html);
    $html = preg_replace_callback('/(<(script).*?>)(.*)(<\/\2>)/s',
      function ($matches) use (&$js) {
        return $matches[1] . trim($js->pack($matches[3]), ';') . $matches[4];
      }, $html);
    $html = preg_replace_callback('/(<(style).*?>)(.*)(<\/\2>)/s',
      function ($matches) use (&$css) {
        return $matches[1] . $css->pack($matches[3]) . $matches[4];
      }, $html);
    $html = preg_replace_callback('/>([^<]*)</', function ($matches) {
      return ">" . trim($matches[1]) . '<';
    }, $html);
    $html = str_ireplace($placeholder, $php, $html);
    return trim($html);
  }

  /**
   * Renders a template.
   *
   * @param string $template @see Template::findTemplate()
   * @param array $params [optional] An array of variables to pass to the
   *                      templating engine.
   * @param string $extension [optional] The templating engine guesses what the
   *                          type of the template file is by inspecting the
   *                          second extension of the template name (eg. "html"
   *                          in "body.html.smarty") this argument allows you to
   *                          override it.
   *                          The actual extension of a file (eg. "smarty")
   *                          should map directly to the name of a registered
   *                          templating engine.
   * @return array The rendered text and the file type.
   */
  public function render($template, $params = array(), $extension = null) {
    if (!isset($params['service'])) $params['service'] = Service::get();
    if (!isset($params['space'])) $params['space'] = ' ';
    if (!isset($params['newline'])) $params['newline'] = "\n";
    $tpl = $this->findTemplate($template);
    $fileNameParts = explode('.', $template);
    $engine = $this->getEngine(array_pop($fileNameParts));
    $fileNameParts = array();
    preg_replace_callback('/(\.\w+)?(\.\w+)?$/',
      function ($match) use (&$fileNameParts) {
        $i = count($match);
        if ($i > 1) while (--$i) {
          array_unshift($fileNameParts, ltrim($match[$i], '.'));
        }
      }, $template);
    array_pop($fileNameParts);
    if (!$extension) {
      $extension = count($fileNameParts) ? array_pop($fileNameParts) :
        $engine->type;
    }
    $this->lastExtension = $extension;
    return array(call_user_func(function () use (&$engine, $tpl, &$params) {
      try {
        $engine->render($tpl, $params);
      } catch (\Exception $e) {
        $engine->onException($e);
      }
      return $engine->result;
    }), $extension);
  }

}